iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
AI Engineering

AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄系列 第 29

Day 29|綠燈到底證明了什麼:假綠燈五形狀

  • 分享至 

  • xImage
  •  

五支儀器各自照著同一台機器,compiler、測試、覆蓋率、trace、封包的光圈互相重疊卻沒蓋滿,沒被照到的角落蹲著一隻蟲

簡短回顧

昨天講對帳迴圈的兩半:收拾不該存在的資源,也把應該存在卻消失的資源補回來。兩邊都做,系統才會回到期望狀態。

今天講一件貫穿這二十九天的事。它出現過至少九次,而我一直到寫這個系列的最後幾天,才發現那九次是同一類事情。

一個綠燈,到底證明了什麼?

先講今天才發生的。

我要確認一件我以為早就成立的事:不同使用者的容器之間,網路是隔開的。

這件事不是我自己動手跑的,是我叫 AI 去驗的。它開了兩個帳號、各建一場,從其中一顆去打另一顆,然後交回兩行:

✓ ping 不通
✓ 連接埠打不到

兩條都綠,寫得清清楚楚。

它沒有停在那裡。後來它自己回頭多做了一步,理由是那個綠燈來得太順。進到容器裡看:

sh: 1: ping: not found

那個容器裡根本沒有 ping。 「不通」是整理後交回來的結論;實際發生的是 shell 回報找不到 ping,探測封包根本沒有送出去。測試裝置壞了,結果卻被記成受測條件成立。

第二條更糟。它打的是對方的終端服務連接埠,而終端程式根本不在那顆容器裡跑,它在外面。那是一扇從來沒有人在聽的門,而「沒人應門」被記成「門是鎖著的」。

真正的答案,是在對方容器裡起一個真的監聽器之後才出現的:

✗ 連上了,收到:TENANT-B-SECRET-BANNER

沒有隔離。

而我要老實講我在這件事裡的位置:我讀到的是一句「隔離成立」,不是上面那兩行輸出。 我沒看過 ping: not found,也沒有機會問那個連接埠是誰在聽。我拿到的是結論,而結論看起來完全合理。

它自己回頭補了那一步。如果它沒有回頭,這件事會以「已經驗過了」的身分留在我的認知裡,而我不會有任何理由回去看它。

那個洞後來補掉了,補法的形狀值得記一句:不是在容器裡多加一條防火牆規則,是每個使用者一張自己的 docker network,他所有的 session 只住在那張網上ADR 0016)。不同使用者的 session 之間,在拓撲上不再具有直接互連的路徑。這件事非得做在網路那一層不可:靠容器裡的防火牆的話,完全開放的那種網路模式就整個漏掉了,而隔離不能只在其中一種模式下成立。

但照今天的規矩,邊界要一起講掉,不然這句話就會比機制大。這只隔開 session 之間的直接連線,不等於提供使用者之間的機密性。同一個人自己的幾場 session 之間是看得到的;而且那些網路上還掛著同一顆收 trace 的容器。共享的 Jaeger 是刻意保留的共同服務,也是目前的例外讀取面:它的查詢介面沒有認證,任何一場 session 都讀得到跨使用者的中繼資料(誰、哪一場、用了多少 token)。那是評估過之後接受的,不是漏的:這套東西從頭到尾沒有宣稱使用者之間有機密性,說明文件講邊界的第一句就是「開帳號給誰,就等於請他信任你」。

至於驗證那一邊,改的是順序:先在對方容器裡真的起一個監聽器、確認連得到,再去驗連不到。 這一步等一下還會再出現一次,它是今天四個對策裡、三個儀器修法的第一個。

那天晚上我回頭翻這二十九天,發現這不是第一次。它跨越 skill、容器、防火牆、網頁平台、終端程式五個層次,每一次我都當成獨立的一個 bug 修掉,從來沒有把它們擺在一起看。

擺在一起之後,它們是同一件事的五種形狀。說好要把一路抓到的假綠燈整理起來,就是這份:假綠燈五形狀

但這五種不是互斥的分類,而是五個診斷角度。同一個假綠燈可能同時踩到兩種;我把案例放在哪一格,看的是最先失效、也最值得優先修掉的那個前提。

第五種不是燈號本身造假,而是真正的紅燈經過人的判讀與記憶,最後留下了同樣的錯誤安全感,所以我仍把它收進假綠燈。

假綠燈五形狀:前四種壞的是儀器,第五種儀器正常,讀的人錯了

第一種:檢查的工具不在場

上面那條 ping 就是。這一種的共同點是:檢查工具沒有成功執行,而工具本身的錯誤被當成受測條件通過。

我在這裡先按最早失效的那一層來分:工具連跑都沒跑起來(指令不存在、二進位檔沒安裝、執行檔路徑打錯),主要放在第一種;探測已經執行,但標的或輸入不成立,主要放在第二種;跑起來後失敗訊號被吞掉,主要放在第四種。這不是三條切得乾乾淨淨的界線,同一個案例可能跨兩格。

它特別會騙人,因為只要錯誤輸出或非零狀態沒有被保留下來,失敗交到人面前就可能只剩一片安靜。指令不存在、二進位檔沒裝、執行檔路徑打錯,明明都是測試裝置故障,最後卻可能被整理成「什麼都沒發生」,再被判讀成沒事。

第二種:檢查的對象不是你以為的那個

開場的第二條就是這一種。連線動作確實執行了,打的卻是一個根本沒有人監聽的連接埠。它量到的是「沒人應門」,不是「門已經鎖上」。

Day 5 那個漏洞掃描也是這一種。Day 13 那張表我把它放在第一種;照上面「最早失效的那一層」來分,Trivy 確實跑起來了,失效的是標的,所以今天把它移到這裡。我對一棵沒有任何依賴清單的樹跑掃描,它回傳成功,吐出一份連 Results 這個鍵都沒有的空報告。差一點就被寫進報告,變成「安全掃描零命中」。

「零命中」與「沒有標的可以掃」是兩件完全不同的事。那次中間差了 31 筆。修法是讓跑掃描的那支腳本在掃完之後確認有沒有東西可以掃,沒有的話報告上要明講「本次無標的」,不准跟「乾淨」共用同一個綠色。

Day 18,我為了不讓憑證進到容器裡,蓋了一整台反向代理。每個零件單獨測都是綠的:代理起得來、設定檔正確、憑證注入成功、上游打得通。

(先講清楚,免得跟前面兩天搞混:那是一顆單機共用的代理,只收 API 呼叫。前面兩天講的「每個使用者一顆」是很後來才有的東西,兩者不是同一個。)

後來用一個真實案例跑了一次全鏈驗證,才發現一件事:代理立在那裡,而整套流程從來沒走過它。

審查用的 API base 一直是從網址推導出來的、直連 GitLab,token 也還是從環境變數讀。代理完全沒有參與。

我測了它能不能運作,沒測它有沒有被使用。 這兩件事之間的距離比想像中遠:一個零件可以完美運作十年,同時完全沒有參與任何事情。

還有一個同種的變形,發生在測試上。有一支測試要驗證某個標記不該出現,而它比對的字串是從別處抄一份過來的。抄的那一刻它是對的;後來來源改了名字,這支測試就一直拿著舊字串去比對「不該出現」,於是永遠綠。它比對的不是現況,是自己。

從那次之後我回到一個核心原則:同一項現況只能有一個單一真相來源(Single Source of Truth,SSOT)。凡是用來代表現況、預期跟單一來源同步的驗證資料,不要另外手抄一份;應直接從來源派生,或建立能抓到兩邊漂移的檢查。Golden 與 fixture 不同,它們是刻意凍結的預期結果,因此重錄必須是明確動作,差異也必須重新審查。

最近又撞到一個同種的,而這次犯錯的是測試自己。我那份 ttyd 的 Rust 移植原本只在 Linux 上跑,拿去 macOS 編才發現,測試裡至少有四處守的不是程式本身,而是跑測試那台機器的性質:loopback 介面在 Linux 叫 lo、在 macOS 叫 lo0--browser 要叫起來的系統開啟程式,一邊是 xdg-open、一邊是 open。其中幾支在 Linux 上一路是綠的,而那個綠說的是「這台機器長這樣」,不是「這段程式做對了」;另有一支更早就壞了,它連在 Linux 上都紅,只是紅的理由跟它要守的東西無關。

這一節前面幾個例子,犯錯的是掃描工具、是那台沒有人走過的代理、是一份抄過來就沒再同步的字串。這一次犯錯的,是那道防線本身。

同一份 ttyd 移植裡還有另一支測試,跟上面那支在 Linux 上也紅的不是同一支,但問題同樣是量錯對象。一支守輸入積壓上限的測試在某一顆 commit 之後變紅,然後一路紅了五個星期,中間包含把整份移植併進主線那一次。它沒有被貼上偶發,也沒有被任何暫時性的解釋消化掉,因為根本沒有人讀它。

這件事值得記成一句話:一支沒有人讀的紅色測試,比一支不存在的測試更糟,因為它還留在清單上。 不存在的測試至少不會讓人以為有東西守著。

它守的那個上限從頭到尾沒有被破過。壞掉的是那支測試量錯了對象:它量的是主機的終端輸入管線在停止接收之前能吞多少,不是那個上限本身,所以我把它放在第二種。後面第五種談的是另一支測試:它量對了,問題出在紅燈後來被怎麼解釋與記住。

第三種:循環論證

Day 22 我想回答一個問題:這台機器有沒有把東西送到不該去的地方?

我有完整的流量紀錄,翻遍了,沒有可疑的連線。結論:沒有外洩。

問題是那份紀錄,是只錄了我允許的那條路產生的。拿一份只包含合法流量的資料,去證明沒有非法流量,那不是證據,是同義反覆。若要回答「這組規則實際擋到了什麼」,證據應該看防火牆的計數器:哪些封包真的命中了拒絕規則。但計數器仍只涵蓋經過這組規則與觀測位置的流量,不能單獨替整台機器宣告沒有外洩。

Day 13 是同一個問題的另一面。我寫了測試案例要驗證一份規則有沒有效,而那些案例的答案,就印在規則本身的教材裡。那不是一道有效的關卡,那是一面鏡子。

這一種最難自己看出來,因為推論的每一步都正確,只有前提是自己放進去的。

我的判準是先問:這份資料在設計上,究竟有沒有可能包含反例? 如果不可能,它就回答不了「有沒有反例」。

第四種:失敗沒有聲音

Day 26 我改寫了一個終端程式,加了一個旗標。原版對它不認得的旗標其實有喊:stderr 上會出現一行 unrecognized option。但控制平面起這支程式的時候,把它的輸出全部導向 /dev/null,不這樣做那條管線寫爆會把程式卡住。聲音發出來了,發在沒有人聽的地方。

而它喊完也不會退出。帶值的那種更麻煩:--title VALUEVALUE 會被當成要執行的命令,後面的參數也跟著被吞進去。若只檢查行程是否還活著,就會誤認為啟動成功;目前的控制平面還會檢查指定 port 上是否真的由 ttyd 提供服務,才擋得住這種假象。

這一格的名字可以寫成一句話:症狀是缺席,而畫面說一切正常。 這句話一開始只是在描述那個終端程式,回頭看,它是整組問題的名字。

這一種在這套系統裡到處都是:

  • 遙測送不出去,沒有任何錯誤。 這條 OTLP 匯出路徑採 fail-open:遙測送不出去時不會阻擋主要工作,也不會直接出現在使用者的操作畫面上。我一度以為遙測正常運作了很久,實際上 session 那張網跟收 trace 的容器之間從來沒有接上過。畫面上唯一的跡象是「沒有資料」,而沒有資料看起來像是沒有人用
  • 一個從來沒有生效過的樣式。 標籤上寫著某個 class,程式卻在掛載時把整個 class 屬性覆蓋掉。用文字搜尋找得到它,執行的時候它不存在。發現它的方式不是搜尋,是有人真的量了一次那個元素實際的 class

最後那一項值得單獨說:文字搜尋對這一族天生盲目。 它看得到「寫了什麼」,看不到「執行的時候還在不在」。

Day 20 那個是這一種裡傷害最大的。一份權限設定裡少了兩個引號,於是那條規則的比對範圍被放寬了。而畫面上照樣印出「防火牆已驗證」,因為驗證的那段程式檢查的是「規則有沒有套用成功」,不是「套用的規則是不是我要的那一條」。

規則套用成功了。錯的規則,成功地套用了。

Day 25 有兩道疤都住在這一格。一道是授權判斷因為求值時機的關係,實際結果是全部拒絕;另一道是內容安全政策寫成「一律禁止嵌入」,抽屜拉開來一片空白,只有瀏覽器的 console 有一行錯誤。兩道的共同點是測試全綠,因為測試打的是後端端點,沒有人打那條真正被使用者走的路徑。而空白看起來像還沒載入完。

第五種:一個自己好了的紅燈

前面四種壞的都是儀器。這一種相反,燈是紅的,而且紅得沒有判錯。

有一支測試在一天之內紅過至少三次。三次都被讀成「這支怪怪的」,最後貼上一個標籤:偶發

讀它的是 AI,貼標籤的也是。而我看到的只有那個標籤,於是我也就接受了。

後來真的去查,先把它拿去每一顆 commit 上各跑一次。結果整齊地貼著 commit 邊界:

前一顆  綠
第 N 顆 🔴
第 N+1  🔴
第 N+2  綠
之後    一路綠

這輪每顆 commit 只跑一次,所以這個結果本身只能把故障範圍縮到那兩顆,還不足以單獨排除偶發。真正讓我確定它不是競態的,是把失敗版本展開之後看到的固定根因:SESSION_NETWORK 已經被刪除,sessions.py 卻還留著九處引用;create() 每次走到那條路徑,都會固定拋出 AttributeError

它不是偶發。那兩顆 commit 上,建立一場 session 的主要路徑對每個人都是壞的。

那為什麼沒有人當場抓到?合理的猜測是訊號太弱。去看了才發現完全相反:那兩顆 commit 上,快速那組測試二十三支紅了十七支,而錯誤輸出裡那個關鍵字出現了五十四次。指名道姓,一點都不含糊。

它沒有被忽略。它被正確地讀成「這棵樹正在重構,改完就好了」。

而那個判斷,在當下是對的。

問題發生在之後:重構真的完成了,但沒有人回頭確認「當時紅的那些,現在為什麼綠了」。 於是那個暫時性的解釋沒有被撤銷,它變成了背景知識。同一支測試下一次再紅的時候,記憶裡只剩「這支以前紅過」,於是它得到一個新標籤:偶發。

一個沒查清楚就消失的紅燈,比一個持續紅的更危險。 持續紅的會逼人處理;自己好了的那個,會把「它會自己好」寫進所有人的直覺裡。

還有一個細節讓這件事更容易發生:那條斷言的失敗訊息只印了例外的型別,沒有印訊息。所以畫面上是這樣一行:

FAIL  恰好一個成功、一個被配額擋下(['boom:AttributeError', 'quota'])

boom:AttributeError 看起來像雜訊。而真正那句話(「某個模組沒有那個屬性」)被丟掉了。把訊息帶上之後,同一行變成:

boom:AttributeError: module '…config' has no attribute 'SESSION_NETWORK'

這一行就更不容易被標成偶發。 差別只是把已經抓到的例外多印二十個字。

最後把這一種跟前面四種的界線畫清楚,因為它比這一種本身更重要。前面四種壞的是儀器:沒跑起來、量錯對象、只量得到自己放進去的東西、失敗不出聲。第五種不一樣:若把儀器限定為紅/綠判定,它其實判對了;真正的紅燈已經出現,也被讀懂。只是診斷輸出沒有帶出例外訊息,讓人更容易用一個暫時性的理由消化它。補齊訊息能降低誤判,最後仍要有人回頭撤銷過期的解釋。所以接下來三個儀器修法不夠,還需要第四個流程對策。

那要怎麼辦?

沒有一勞永逸的答案,但對付假綠燈五形狀,這二十九天下來有三個修儀器的做法,加上一個修判讀流程的做法。

先驗正例

今天那兩個假綠燈,只要多一步就會當場破功:動手驗「連不到」之前,先驗一次「連得到」。

如果連應該通的那條都沒反應,那代表測試裝置壞了,不是被測的東西通過了。

這一步很便宜,而且能攔住第一種裡很大一部分。但正反例必須共用同一套探測工具與關鍵路徑,只改變真正要驗的那個條件;若兩邊換了工具或標的,正例亮綠也不能替反例的測試裝置背書。我現在寫任何「應該不通」的檢查,都會配一條共用同一個監聽器與連線方式的「應該通」在前面,而且那一條沒過就當場失敗、講清楚是裝置的問題。

昨天那個 fd 的回歸測試,第一條斷言先驗「只關包裝物件時 fd 還活著」,就是同一件事的另一個用法:寫完斷言之後,故意把被測的東西弄壞一次,確認它真的會紅。

要驗執行時效果,證據就得靠近實際使用點

「這個規則有沒有生效」不要靠搜尋標籤,要去量元素實際的樣式或尺寸。「這條路有沒有被走過」不要看設定,要看計數器。「服務起來了沒」不要看行程在不在,要真的送一個請求進去。昨天那個 docker ps 秒回、inspect 卻卡死的落差,講的就是這件事。

文字搜尋能回答指定範圍的原始碼裡有沒有某段文字,不能單獨證明執行時的效果。要回答「規則有沒有生效」,還是得從實際使用的位置量。

讓失敗發出聲音,而且探測的位置要對

fail-open 的東西一定要有人替它出聲。做不到,就至少把「幾點確認過」寫在畫面上,讓看的人自己判斷這個資訊有多新。這是昨天那個列表的做法,而它救的正是「看起來正常」這件事。

還有一個更小但更常犯的:檢查的位置要跟實際使用的位置一致。

今天修遙測的時候我差點犯這個錯。探測是從控制平面發出的,而真正要送資料的是另一個容器、在另一張網路上。控制平面探得到,完全不代表那邊送得出去。

探測與現實脫節,比探測失敗更難查,因為前者會給你一個綠燈。

暫時性的解釋必須有到期日

第五種的紅燈其實判對了,問題出在「重構中,等一下就會好」這個解釋完成任務後沒有被撤銷。暫時忽略可以是合理決定,但它必須同時留下回頭條件:什麼時候重跑、由誰確認,以及綠回來時要查清楚是修好了,還是剛好不再觸發。

沒有這個到期日,今天合理的例外會變成明天的背景知識;下一次同一支測試再紅,人只會記得「它以前也紅過」。

四個對策

  1. 先驗正例。 先確認測試裝置真的能測到「通」,再相信它量出的「不通」
  2. 量執行結果,不只搜尋設定。 設定存在、行程活著或產物躺在那裡,都不等於實際路徑用了它
  3. 讓失敗發聲,而且探測點與使用點一致。 否則量到的只是另一個位置的綠燈
  4. 暫時性解釋必須有到期日。 好了之後仍要回頭確認,它究竟為什麼好了

這四個對策共用的那一個問題

四個對策不是四張分開的清單。它們回答的是同一個問題,而那個問題可以在動手之前就先問一次:

這盞綠燈,我看過它紅嗎?

這句話要展開才完整:我是否刻意破壞了這次要驗證的條件,並看見同一條觀測路徑因此轉紅? 單純看過紅燈還不夠;如果換了工具、標的或路徑,紅的可能是另一件事。

前四種形狀在這個問題底下會直接現形:ping 沒有成功執行,紅的是測試裝置而不是網路隔離(第一種);監聽器不在對面那顆容器上,紅或綠都不是對那個對象量出來的(第二種);前提是自己放進去的,資料結構上不可能包含反例(第三種);它其實紅過,只是喊在沒有人聽的地方,我根本沒有收到那個訊號(第四種)。

第五種要把問題換一半。那盞燈我看過它紅,它後來自己變綠了,所以第五種要問的是:它從紅變綠的那一次,我確認過原因嗎? 這就是上面那個到期日在問的事:是修好了,還是剛好不再觸發。

這個動作我不是今天才開始做。Day 5 那份空報告之後,掃描多了一道「有沒有標的」的確認;上一節那支 fd 的回歸測試,先驗的也是前提成立。做的其實是同一件事,差別只在於我一直把它當成一個習慣在做,沒有把它寫成一句可以在動手之前先問的話。

寫成一句之後有個附帶的好處:它問的是我做過什麼,不是這個系統對不對。「這個東西應該沒問題吧」沒有答案;「這盞燈我看過它紅嗎」有,而且答案只有兩個。

完稿後的回歸驗證

這篇原稿寫完後,我剛好把同一套前端從 Jinja 與手寫 JavaScript 改成 Vue 3。同一場遷移裡,三種證據問題依序又出現一次:

發生的事 證據哪裡壞了 後來怎麼改
我先判斷抽屜動畫讓終端量到錯誤尺寸 逐幀探針推翻了診斷:尺寸一直正確;真正沒有因果關係的是只靠 300ms debounce 發出的請求。動畫拉到 800ms 時,它會在結束前 577ms 送出 先量再判,不能因為修法方向接近,就留下錯的理由
golden 模組在被 import 時就開始重錄 任何人只想借用函式,都可能先把預期答案覆寫成眼前輸出,後面的比對自然全綠 重錄只能明確執行,差異必須重新審查;答案不能由作答者順手改寫
e2e 撿到一份已存在、但比原始碼舊的 dist 測試確實在跑,對象卻不是眼前這份程式碼的產物 來源比產物新就先重建;「產物存在」不再被當成「產物正確」

它們不是三個「Vue 的坑」,而是同一場 AI 改寫再次撞上的證據問題,也跟 Day 26 的 Rust 改寫 是同一個形狀:改寫的驗收不是多一盞新的綠燈,而是每一條差異都交代得掉。完整過程放在公開 repo 的 frontend-vue3-journal.md,最後留下的設計決定則在 ADR 0020

本日小結

走到這裡,我不再把 compiler、測試、覆蓋率、trace 或封包紀錄看成彼此競爭的答案。它們是觀察同一個系統的不同儀器,而每一支都有自己的視野:compiler 能攔靜態語意與型別問題,測試只碰我寫出的情境,覆蓋率只說某條路走過、不說結果正確,trace 只看得到有埋點的步驟,封包紀錄也只涵蓋實際經過側錄位置的流量。

把這些證據疊起來,我能逐步縮小錯誤藏身的空間;但任何一支儀器單獨亮綠,都不能替整個系統宣告正確。多數時候,我真正能說的不是「已經證明它對」,而是:在目前這些探測方法與適用範圍內,我還沒有找到足以推翻它的反例。 如果現有儀器根本回答不了問題,「無法判定」才是正確輸出,不能為了交差把它塗成綠燈。

綠燈只證明一件事:我檢查過的那個部分,在我檢查的那個方式下,沒有出現我預期的那種失敗。

這句話很長,而每一個限定詞都是必要的。前四種形狀分別打掉其中一個:

  1. 檢查的工具不在場。那個「方式」沒有成功執行,工具錯誤卻被讀成受測條件通過
  2. 檢查的對象不對。「那個部分」不是你以為的部分,零件是好的,但沒有人在用它
  3. 循環論證。推論每一步都對,只有前提是自己放進去的
  4. 失敗沒有聲音。「預期的那種失敗」不包含靜默,而許多最難察覺的失敗都是安靜的

第五種不打限定詞,它打的是人:紅燈出現了、也被讀懂了,然後被一個暫時性的解釋消化掉,而那個解釋過期之後沒有人撤銷它。 每一步當下都對,合起來卻讓一個真的破口帶著「偶發」的標籤留在那裡,直到有人回頭一顆一顆 commit 去查。

所以對策分成兩層:先驗正例、量執行結果、讓失敗發聲且讓探測點貼著使用點,修的是儀器;替暫時性解釋設定到期日,修的是判讀流程。第五種的紅/綠判定本來就沒有判錯,不能只靠前三條處理。

這二十九天修過的東西裡,最不好處理的不是單純會噴錯誤的。錯誤在叫,至少還有機會被看見;但正如那支紅了五週的測試,叫了也不等於有人在聽。

真正麻煩的是那些看起來正常、而且會讓下一個人放棄檢查的:一個綠燈、一份空報告、一句寫在註解裡但其實沒做到的宣稱。它們不會叫,所以可以待很久,而且每多待一天,就多一個人把它當成已經確認過的事實。假綠燈五形狀收的,就是這些不會叫的東西。

這三十天的程式碼、測試與每一天的對照,都在公開的 repo 裡:nathanfhh/30-days-ironman

所以最後一天,我不再問哪一盞綠燈能替整套系統宣告正確;我要問的是,證據永遠有限時,哪些判斷仍能跟著人走到下一個場域。


上一篇
Day 28|收得掉,也長得回來才算平台:對帳迴圈與 40 分鐘全站卡死
下一篇
Day 30|換了場域還帶得走的那些:從 Code Review 到 Agent 治理,附關聯圖與向量導讀
系列文
AI 的駕馭之道:一個 AI Code Reviewer 的養成、評測與邊界實錄30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言